{*******************************************************************************
* The contents of this file are used with permission, subject to the Mozilla   *
* Public License Version 1.1 (the "License"); you may not use this file except *
* in compliance with the License. You may obtain a copy of the License at      *
* http://www.mozilla.org/MPL/                                                  *
*                                                                              *
* Software distributed under the License is distributed on an "AS IS" basis,   *
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for *
* the specific language governing rights and limitations under the License.    *
*                                                                              *
* The Original Code is distributed as part of the "mediate" product and is     *
* Copyright (C) @home media limited. All Rights Reserved.                      *
*                                                                              *
* Email: support@athomemedia.co.uk                                             *
* Web:   http://www.athomemedia.co.uk                                          *
*******************************************************************************}

unit Dials;

interface

uses Classes, UIObjects, XMLHelper;

{$IFDEF DESIGNER}
const
  // Property categories and hints for designer
  CAT_DESIGN = 'Design';
  CAT_KNOB = 'Dial Knob';

  HINT_ALIGN = 'specifies optional position alignment for the dial knob';
  HINT_KNOBWIDTH = 'specifies width of the dial knob';
  HINT_KNOBHEIGHT = 'specifies height of the dial knob';
  HINT_KNOBX = 'specifies the knob horizontal position within the dial';
  HINT_KNOBY = 'specifies the knob vertical position within the dial';
  HINT_ROTATEX = 'specifies the horizontal axis of rotation for the knob';
  HINT_ROTATEY = 'specifies the vertical axis of rotation for the knob';
  HINT_KNOBIMAGES = 'defines bitmaps and shapes to use for drawing the dial knob in each state';
  HINT_STARTANGLE = 'specifies the start angle in degrees for the dial knob movement';
  HINT_SWEEPANGLE = 'specifies the sweep angle in degrees for the dial knob movement';
  HINT_BACKGROUNDS = 'defines bitmaps and shapes to use for drawing the dial background in each state';
  HINT_WIDTH = 'specifies width for the dial';
  HINT_HEIGHT = 'specifies height for the dial';
{$ENDIF}

type
  TAHMKnobAlign = (kaNone, kaCenter, kaTopLeft, kaTop, kaTopRight, kaRight,
                   kaBottomRight, kaBottom, kaBottomLeft, kaLeft);

  TAHMDial = class(TAHMUIDisplayObject)
  private
    FAligning: Boolean;
    FKnobAlign: TAHMKnobAlign;
    FKnobWidth: Single;
    FKnobHeight: Single;
    FKnobCenterX: Single;
    FKnobCenterY: Single;
    FRotateCenterX: Single;
    FRotateCenterY: Single;
    FStartAngle: Integer;
    FSweepAngle: Integer;
    FVirtKnobWidth: Integer;
    FVirtKnobHeight: Integer;
    FVirtKnobCenterX: Integer;
    FVirtKnobCenterY: Integer;
    FVirtRotateCenterX: Integer;
    FVirtRotateCenterY: Integer;
    FBackgroundImages: TAHMUIStateObjectList;
    FKnobImages: TAHMUIStateObjectList;
  protected
    function Align2Str(Value: TAHMKnobAlign): String;
    function Str2Align(Value: String): TAHMKnobAlign;
    procedure SetKnobAlign(Align: TAHMKnobAlign);
    procedure SetVirtualKnobWidth(Width: Integer);
    procedure SetVirtualKnobHeight(Height: Integer);
    procedure SetVirtualKnobCenterX(X: Integer);
    procedure SetVirtualKnobCenterY(Y: Integer);
    procedure SetVirtualRotateCenterX(X: Integer);
    procedure SetVirtualRotateCenterY(Y: Integer);
    procedure SetVirtualWidth(Width: Integer); override;
    procedure SetVirtualHeight(Height: Integer); override;
    procedure SetStartAngle(StartAngle: Integer);
    procedure SetSweepAngle(SweepAngle: Integer);
    procedure AlignKnob;
    procedure ClearProperties; override;
  public
    constructor Create(const Name: String); override;
    destructor Destroy; override;
    property KnobWidth: Single read FKnobWidth write FKnobWidth;
    property KnobHeight: Single read FKnobHeight write FKnobHeight;
    property KnobCenterX: Single read FKnobCenterX write FKnobCenterX;
    property KnobCenterY: Single read FKnobCenterY write FKnobCenterY;
    property RotateCenterX: Single read FRotateCenterX write FRotateCenterX;
    property RotateCenterY: Single read FRotateCenterY write FRotateCenterY;
    procedure ConfigurePreview(Control: TObject); override;
    procedure Render(Control: TObject; iX, iY, iWidth, iHeight: Single); override;
    procedure SeedObject; override;
    procedure Assign(Source: TPersistent); override;
    procedure SaveXMLBody(Lines: TStrings); override;
    procedure ParseFromXML(XML: TAHMXMLHelper); override;
    procedure BuildProperties(Properties: TAHMUIPropertyList); override;
  published
    property KnobAlign: TAHMKnobAlign read FKnobAlign write SetKnobAlign;
    property VirtualKnobWidth: Integer read FVirtKnobWidth write SetVirtualKnobWidth;
    property VirtualKnobHeight: Integer read FVirtKnobHeight write SetVirtualKnobHeight;
    property VirtualKnobCenterX: Integer read FVirtKnobCenterX write SetVirtualKnobCenterX;
    property VirtualKnobCenterY: Integer read FVirtKnobCenterY write SetVirtualKnobCenterY;
    property VirtualRotateCenterX: Integer read FVirtRotateCenterX write SetVirtualRotateCenterX;
    property VirtualRotateCenterY: Integer read FVirtRotateCenterY write SetVirtualRotateCenterY;
    property StartAngle: Integer read FStartAngle write SetStartAngle;
    property SweepAngle: Integer read FSweepAngle write SetSweepAngle;
    property BackgroundImages: TAHMUIStateObjectList read FBackgroundImages;
    property KnobImages: TAHMUIStateObjectList read FKnobImages;
    // Publish inherited properties
    property VirtualWidth;
    property VirtualHeight;
  end;

  TAHMDialList = class(TAHMUIObjectList)
  private
  protected
  public
    constructor Create; override;
    function AddDial(const InheritFrom, NewName: String): TAHMDial;
    function GetDial(const Name: String): TAHMDial;
    procedure PopulateDefaultObjects; override;
  end;

implementation

uses SysUtils, ApplicationManager, Screen, BaseControls, UIControls, Bitmaps,
     Shapes, MetaConsts, TagNames, XMLTags, ErrorCodes, AHMTypes;

// TAHMDialList

constructor TAHMDialList.Create;
begin
  inherited Create;

  XMLTagName := TAG_DIALS;
  ChildClass := TAHMDial;
end;

function TAHMDialList.AddDial(const InheritFrom, NewName: String): TAHMDial;
begin
  Result := TAHMDial(Inherit(InheritFrom, NewName));
end;

function TAHMDialList.GetDial(const Name: String): TAHMDial;
begin
  Result := TAHMDial(GetObject(Name));
end;

procedure TAHMDialList.PopulateDefaultObjects;
begin
  with ApplicationManagerInstance.ThemeManager, DefaultObject as TAHMDial do
  begin
    VirtualWidth := DEFAULT_CTRL_HEIGHT;
    VirtualHeight := DEFAULT_CTRL_HEIGHT;
    VirtualKnobCenterX := DEFAULT_CTRL_HEIGHT div 4;
    VirtualKnobCenterY := DEFAULT_CTRL_HEIGHT div 4;
    SweepAngle := 360;
    BackgroundImages.AddObject(STATE_VAL_IDLE, ThemeObjects[otShapes].DefaultObjects[TAG_BACKGROUND]);
    KnobImages.AddObject(STATE_VAL_IDLE, ThemeObjects[otShapes].DefaultObjects[TAG_DIAL]);
  end;
end;

//----------------------------------------------------------------------------//

// TAHMDial

constructor TAHMDial.Create(const Name: String);
begin
  inherited Create(Name);

  XMLTagName := TAG_DIAL;
  ObjectType := otDials;
  ImageIndex := Ord(otDials);

  FBackgroundImages := TAHMUIStateObjectList.Create;
  FBackgroundImages.ObjectTypes := [otBitmaps, otShapes];
  FBackgroundImages.StateType := stControl;
  FBackgroundImages.Owner := Self;
  FKnobImages := TAHMUIStateObjectList.Create;
  FKnobImages.ObjectTypes := [otBitmaps, otShapes];
  FKnobImages.StateType := stControl;
  FKnobImages.Owner := Self;

{$IFDEF DESIGNER}
  FKnobImages.DisplayClassName := 'Dial knob image';
  FBackgroundImages.DisplayClassName := 'Dial background image';

  // Render preview using a dial control
  PreviewControlClass := TAHMDialControl;
{$ENDIF}

  ClearProperties;
end;

destructor TAHMDial.Destroy;
begin
  FBackgroundImages.Free;
  FKnobImages.Free;

  inherited Destroy;
end;

function TAHMDial.Align2Str(Value: TAHMKnobAlign): String;
begin
  if Value = kaCenter then Result := ATTR_VAL_CENTER
  else if Value = kaTopLeft then Result := ATTR_VAL_TOPLEFT
  else if Value = kaTop then Result := ATTR_VAL_TOP
  else if Value = kaTopRight then Result := ATTR_VAL_TOPRIGHT
  else if Value = kaRight then Result := ATTR_VAL_RIGHT
  else if Value = kaBottomRight then Result := ATTR_VAL_BOTTOMRIGHT
  else if Value = kaBottom then Result := ATTR_VAL_BOTTOM
  else if Value = kaBottomLeft then Result := ATTR_VAL_BOTTOMLEFT
  else if Value = kaLeft then Result := ATTR_VAL_LEFT
  else Result := '';
end;

function TAHMDial.Str2Align(Value: String): TAHMKnobAlign;
begin
  if Value = ATTR_VAL_CENTER then Result := kaCenter
  else if Value = ATTR_VAL_TOPLEFT then Result := kaTopLeft
  else if Value = ATTR_VAL_TOP then Result := kaTop
  else if Value = ATTR_VAL_TOPRIGHT then Result := kaTopRight
  else if Value = ATTR_VAL_RIGHT then Result := kaRight
  else if Value = ATTR_VAL_BOTTOMRIGHT then Result := kaBottomRight
  else if Value = ATTR_VAL_BOTTOM then Result := kaBottom
  else if Value = ATTR_VAL_BOTTOMLEFT then Result := kaBottomLeft
  else if Value = ATTR_VAL_LEFT then Result := kaLeft
  else Result := kaNone;
end;

procedure TAHMDial.SetKnobAlign(Align: TAHMKnobAlign);
begin
  FKnobAlign := Align;
  AlignKnob;
end;

procedure TAHMDial.SetVirtualKnobWidth(Width: Integer);
begin
  FVirtKnobWidth := Width;
  FKnobWidth := ScreenInstance.ScaleVirtualX(Width);
end;

procedure TAHMDial.SetVirtualKnobHeight(Height: Integer);
begin
  FVirtKnobHeight := Height;
  FKnobHeight := ScreenInstance.ScaleVirtualY(Height);
end;

procedure TAHMDial.SetVirtualKnobCenterX(X: Integer);
begin
  FVirtKnobCenterX := X;
  FKnobCenterX := ScreenInstance.ScaleVirtualX(X);
  if not FAligning then FKnobAlign := kaNone;
end;

procedure TAHMDial.SetVirtualKnobCenterY(Y: Integer);
begin
  FVirtKnobCenterY := Y;
  FKnobCenterY := ScreenInstance.ScaleVirtualY(Y);
  if not FAligning then FKnobAlign := kaNone;
end;

procedure TAHMDial.SetVirtualRotateCenterX(X: Integer);
begin
  FVirtRotateCenterX := X;
  FRotateCenterX := ScreenInstance.ScaleVirtualX(X);
end;

procedure TAHMDial.SetVirtualRotateCenterY(Y: Integer);
begin
  FVirtRotateCenterY := Y;
  FRotateCenterY := ScreenInstance.ScaleVirtualY(Y);
end;

procedure TAHMDial.SetVirtualWidth(Width: Integer);
begin
  inherited;
  AlignKnob;
end;

procedure TAHMDial.SetVirtualHeight(Height: Integer);
begin
  inherited;
  AlignKnob;
end;

procedure TAHMDial.SetStartAngle(StartAngle: Integer);
begin
  FStartAngle := Abs(StartAngle) mod 360;
  ApplicationManagerInstance.UIManager.InvalidateUIObject(Self);
end;

procedure TAHMDial.SetSweepAngle(SweepAngle: Integer);
begin
  FSweepAngle := Abs(SweepAngle) mod 360;
  ApplicationManagerInstance.UIManager.InvalidateUIObject(Self);
end;

procedure TAHMDial.AlignKnob;
begin
  // Avoid recursive alignment calls
  if FAligning then Exit else FAligning := True;
  try
    // Auto align knob
    case FKnobAlign of
      kaCenter: begin
        VirtualKnobCenterX := VirtualWidth div 2;
        VirtualKnobCenterY := VirtualHeight div 2;
      end;
      kaTopLeft: begin
        VirtualKnobCenterX := 0;
        VirtualKnobCenterY := 0;
      end;
      kaTop: begin
        VirtualKnobCenterX := VirtualWidth div 2;
        VirtualKnobCenterY := 0;
      end;
      kaTopRight: begin
        VirtualKnobCenterX := VirtualWidth;
        VirtualKnobCenterY := 0;
      end;
      kaRight: begin
        VirtualKnobCenterX := VirtualWidth;
        VirtualKnobCenterY := VirtualHeight div 2;
      end;
      kaBottomRight: begin
        VirtualKnobCenterX := VirtualWidth;
        VirtualKnobCenterY := VirtualHeight;
      end;
      kaBottom: begin
        VirtualKnobCenterX := VirtualWidth div 2;
        VirtualKnobCenterY := VirtualHeight;
      end;
      kaBottomLeft: begin
        VirtualKnobCenterX := 0;
        VirtualKnobCenterY := VirtualHeight;
      end;
      kaLeft: begin
        VirtualKnobCenterX := 0;
        VirtualKnobCenterY := VirtualHeight div 2;
      end;
    end;
  finally
    FAligning := False;
  end;
end;

procedure TAHMDial.ClearProperties;
begin
  inherited;

  // Clear custom dial properties
  VirtualKnobWidth := 0;
  VirtualKnobHeight := 0;
  VirtualKnobCenterX := 0;
  VirtualKnobCenterY := 0;
  VirtualRotateCenterX := 0;
  VirtualRotateCenterY := 0;
  FKnobAlign := kaCenter;
  FStartAngle := 0;
  FSweepAngle := 0;
  FBackgroundImages.Clear;
  FKnobImages.Clear;
end;

procedure TAHMDial.ConfigurePreview(Control: TObject);
begin
{$IFDEF DESIGNER}
  with Control as TAHMDialControl do
  begin
    // Setup metadata for Dial
    MetaValue := META_PREVIEW_DIAL_VALUE;
    MetaGroup := META_PREVIEW_DIAL_MAX;

    // Don't animate if user is manually dragging
    if Pressed then Exit;

    // MetaIndex is used to store the movement direction
    if (MetaIndex = '') or (Position = 0) then MetaIndex := 'up'
    else if Position = Maximum then MetaIndex := 'down';

    // Increment metadata value to animate Dial control for preview
    with ApplicationManagerInstance do
      if MetaIndex = 'up' then MetaDataManager.IncMeta(MetaValue, MetaGroup)
                          else MetaDataManager.DecMeta(MetaValue, MetaGroup);
  end;
{$ENDIF}
end;

procedure TAHMDial.Render(Control: TObject; iX, iY, iWidth, iHeight: Single);
var
  GfxBg, GfxKnob: TAHMUIDisplayObject;
begin
  // Pre-render our control if necessary
  inherited;

  with Control as TAHMDialControl do
  begin
    // Lookup & cache objects for various dial graphics we'll be using
    GfxBg := TAHMUIDisplayObject(FBackgroundImages.GetStateObject(ControlState, Checked));
    GfxKnob := TAHMUIDisplayObject(FKnobImages.GetStateObject(ControlState, Checked));

    // Render dial background
    if Assigned(GfxBg) then
      GfxBg.Render(Control, iX, iY, iWidth, iHeight);

    if Assigned(GfxKnob) then
      with Viewport.Canvas do
      begin
        // Calculate 3D rotation origin for knob
        Set3DRotateOrigin(FRotateCenterX, FRotateCenterY, FKnobWidth, FKnobHeight);

        // Calculate rotation in Z axis for pointer
        Set3DRotation(0.0, 0.0, RenderValue);

        // Calculate absolute knob position
        iX := iX + FKnobCenterX;
        iY := iY + FKnobCenterY;

        // Render knob graphic
        GfxKnob.Render(Control, iX, iY, FKnobWidth, FKnobHeight);

        // Revert back to 2D rendering mode
        UnSet3DRotation;
      end;
  end;
end;

procedure TAHMDial.SeedObject;
begin
{$IFDEF DESIGNER}
  if Ancestor = nil then
    with ApplicationManagerInstance.ThemeManager.VisualDescriptor do
    begin
      VirtualWidth := DEFAULT_CTRL_HEIGHT;
      VirtualHeight := DEFAULT_CTRL_HEIGHT;
      VirtualKnobWidth := DEFAULT_CTRL_HEIGHT div 2;
      VirtualKnobHeight := DEFAULT_CTRL_HEIGHT div 2;
      VirtualKnobCenterX := DEFAULT_CTRL_HEIGHT div 4;
      VirtualKnobCenterY := DEFAULT_CTRL_HEIGHT div 4;
      SweepAngle := 360;
      BackgroundImages.AddObject(STATE_VAL_IDLE, ShapeList.GetShape(DEFAULT_CIRCLE_NAME));
      KnobImages.AddObject(STATE_VAL_IDLE, ShapeList.GetShape(DEFAULT_CIRCLE_NAME));
    end;
{$ENDIF}
end;

procedure TAHMDial.Assign(Source: TPersistent);
begin
  inherited Assign(Source);

  with Source as TAHMDial do
  begin
    Self.VirtualKnobWidth := VirtualKnobWidth;
    Self.VirtualKnobHeight := VirtualKnobHeight;
    Self.VirtualKnobCenterX := VirtualKnobCenterX;
    Self.VirtualKnobCenterY := VirtualKnobCenterY;
    Self.VirtualRotateCenterX := VirtualRotateCenterX;
    Self.VirtualRotateCenterY := VirtualRotateCenterY;
    Self.KnobAlign := KnobAlign;
    Self.StartAngle := StartAngle;
    Self.SweepAngle := SweepAngle;
    Self.BackgroundImages.Assign(BackgroundImages);
    Self.KnobImages.Assign(KnobImages);
  end;
end;

procedure TAHMDial.SaveXMLBody(Lines: TStrings);
var
  oAncs: TAHMDial;
  oObj: TAHMUIObject;
  i: Integer;
  sWork: String;
begin
  // Determine which ancestor to compare against - real or empty
  oAncs := TAHMDial(ObjectToCompare);

  // Store knob properties
  Lines.Add(TABS[3] + TGB+TAG_KNOB+TGC);

  // Do we need to store alignment?
  if (FKnobAlign <> kaCenter) and (FKnobAlign <> oAncs.KnobAlign) then
    Lines.Add(TABS[4] + TGB+TAG_ALIGNMENT+TGC + Align2Str(FKnobAlign) + TGE+TAG_ALIGNMENT+TGC);

  // Do we need to store knob size?
  if ((FVirtKnobWidth <> 0) and (FVirtKnobWidth <> oAncs.VirtualKnobWidth)) or
     ((FVirtKnobHeight <> 0) and (FVirtKnobHeight <> oAncs.VirtualKnobHeight)) then
    Lines.Add(TABS[4] + TGB+TAG_SIZE+TGC+ IntToStr(FVirtKnobWidth) + ',' +
              IntToStr(FVirtKnobHeight) + TGE+TAG_SIZE+TGC);

  // Do we need to store knob position?
  if ((FVirtKnobCenterX <> 0) and (FVirtKnobCenterX <> oAncs.VirtualKnobCenterX)) or
     ((FVirtKnobCenterY <> 0) and (FVirtKnobCenterY <> oAncs.VirtualKnobCenterY)) then
    Lines.Add(TABS[4] + TGB+TAG_CENTER+TGC + IntToStr(FVirtKnobCenterX) + ',' +
              IntToStr(FVirtKnobCenterY) + TGE+TAG_CENTER+TGC);

  // Do we need to store knob rotation axis?
  if ((FVirtRotateCenterX <> 0) and (FVirtRotateCenterX <> oAncs.VirtualRotateCenterX)) or
     ((FVirtRotateCenterY <> 0) and (FVirtRotateCenterY <> oAncs.VirtualRotateCenterY)) then
    Lines.Add(TABS[4] + TGB+TAG_ROTATE+TGC + IntToStr(FVirtRotateCenterX) + ',' +
              IntToStr(FVirtRotateCenterY) + TGE+TAG_ROTATE+TGC);

  // Do we need to store knob images?
  if FKnobImages.IsDifferent(oAncs.KnobImages) then
  begin
    Lines.Add(TABS[4] + TGB+TAG_STATES+TGC);
    for i := 0 to High(ControlStateNames) do
    begin
      oObj := FKnobImages.GetState(ControlStateNames[i]);
      if Assigned(oObj) then
      begin
        sWork := TGB+TAG_STATE+SPC+ATTR_NAME+ATB + EncodeXML(ControlStateNames[i]) + ATC;
        Lines.Add(TABS[5] + sWork + oObj.XMLReference + TGE+TAG_STATE+TGC);
      end;
    end;
    Lines.Add(TABS[4] + TGE+TAG_STATES+TGC);
  end;

  Lines.Add(TABS[3] + EndTag(TAG_KNOB));

  // Do we need to store background images?
  if FBackgroundImages.IsDifferent(oAncs.BackgroundImages) then
  begin
    Lines.Add(TABS[3] + TGB+TAG_BACKGROUND+TGC);
    Lines.Add(TABS[4] + TGB+TAG_STATES+TGC);
    for i := 0 to High(ControlStateNames) do
    begin
      oObj := FBackgroundImages.GetState(ControlStateNames[i]);
      if Assigned(oObj) then
      begin
        sWork := TGB+TAG_STATE+SPC+ATTR_NAME+ATB + EncodeXML(ControlStateNames[i]) + ATC;
        Lines.Add(TABS[5] + sWork + oObj.XMLReference + TGE+TAG_STATE+TGC);
      end;
    end;
    Lines.Add(TABS[4] + TGE+TAG_STATES+TGC);
    Lines.Add(TABS[3] + TGE+TAG_BACKGROUND+TGC);
  end;

  // Do we need to store start and sweep angle?
  if ((FStartAngle <> 0) and (FStartAngle <> oAncs.StartAngle)) or
     ((FSweepAngle <> 0) and (FSweepAngle <> oAncs.SweepAngle)) then
  begin
    sWork := TGB+TAG_GEOMETRY;
    if (FStartAngle <> 0) and (FStartAngle <> oAncs.StartAngle) then
      sWork := sWork + SPC+ATTR_ANGLE+ATB + IntToStr(FStartAngle) + ATQ;
    if (FSweepAngle <> 0) and (FSweepAngle <> oAncs.SweepAngle) then
      sWork := sWork + SPC+ATTR_SWEEP+ATB + IntToStr(FSweepAngle) + ATQ;
    Lines.Add(TABS[3] + sWork + ETE);
  end;

  // Do we need to store size?
  if ((VirtualWidth <> 0) and (VirtualWidth <> oAncs.VirtualWidth)) or
     ((VirtualHeight <> 0) and (VirtualHeight <> oAncs.VirtualHeight)) then
    Lines.Add(TABS[3] + TGB+TAG_SIZE+TGC+ IntToStr(VirtualWidth) + ',' +
              IntToStr(VirtualHeight) + TGE+TAG_SIZE+TGC);
end;

procedure TAHMDial.ParseFromXML(XML: TAHMXMLHelper);
  procedure ParseStates(RootNode: TAHMXMLNode; StateList: TAHMUIStateObjectList);
  var
    sState, sDest: String;
    StateNode, DetailNode: TAHMXMLNode;
  begin
    // Handle knob states
    StateNode := nil;
    while XML.FindElement(RootNode, StateNode, TAG_STATE) do
      with ApplicationManagerInstance.ThemeManager.VisualDescriptor do
      begin
        sState := XML.GetAttribute(ATTR_NAME);
        DetailNode := nil;

        // Should only be one state property, but need to iterate all nodes
        while XML.NextElement(StateNode, DetailNode) do
        begin
          sDest := XML.GetAttribute(ATTR_NAME);
          if XML.CurrentElement = TAG_SHAPE then
            StateList.AddObject(sState, ShapeList.GetShape(sDest))
          else if XML.CurrentElement = TAG_BITMAP then
            StateList.AddObject(sState, BitmapList.GetBitmap(sDest))
          else
            LogParsingError(THM_PRS_VIS_UNSUPPORTED_DIAL_STATE_TYPE, Self);
        end;
      end;
  end;
var
  sAlign, sStart, sSweep: String;
  iX, iY: Integer;
  DialNode, PropsNode, DetailNode: TAHMXMLNode;
begin
  // Initialise parsing nodes
  DialNode := XML.CurrentNode;
  PropsNode := nil;

  // All Dial elements are optional, so see what we have
  while XML.NextElement(DialNode, PropsNode) do
  begin
    if XML.CurrentElement = TAG_KNOB then
    begin
      // Handle Dial knob properties
      DetailNode := nil;
      while XML.NextElement(PropsNode, DetailNode) do
      begin
        if XML.CurrentElement = TAG_ALIGNMENT then
        begin
          sAlign := XML.GetValue;
          if sAlign <> '' then KnobAlign := Str2Align(sAlign);
        end
        else if XML.CurrentElement = TAG_SIZE then
        begin
          if XML.GetCoordsValue(iX, iY) then
          begin
            VirtualKnobWidth := iX;
            VirtualKnobHeight := iY;
          end
          else
            LogParsingError(THM_PRS_VIS_MALFORMED_DIAL_SIZE_TAGS, Self);
        end
        else if XML.CurrentElement = TAG_CENTER then
        begin
          if XML.GetCoordsValue(iX, iY) then
          begin
            VirtualKnobCenterX := iX;
            VirtualKnobCenterY := iY;
          end
          else
            LogParsingError(THM_PRS_VIS_MALFORMED_DIAL_CENTER_TAGS, Self);
        end
        else if XML.CurrentElement = TAG_ROTATE then
        begin
          if XML.GetCoordsValue(iX, iY) then
          begin
            VirtualRotateCenterX := iX;
            VirtualRotateCenterY := iY;
          end
          else
            LogParsingError(THM_PRS_VIS_MALFORMED_DIAL_ROTATE_TAGS, Self);
        end
        else if XML.CurrentElement = TAG_STATES then
        begin
          ParseStates(DetailNode, FKnobImages);
        end
        else
          LogParsingError(THM_PRS_VIS_UNSUPPORTED_DIAL_KNOB_PROP, Self);
      end;
    end
    else if XML.CurrentElement = TAG_BACKGROUND then
    begin
      // Handle Dial background properties
      DetailNode := nil;
      while XML.NextElement(PropsNode, DetailNode) do
      begin
        if XML.CurrentElement = TAG_STATES then
        begin
          ParseStates(DetailNode, FBackgroundImages);
        end
        else
          LogParsingError(THM_PRS_VIS_UNSUPPORTED_DIAL_BKG_PROP, Self);
      end;
    end
    else if XML.CurrentElement = TAG_GEOMETRY then
    begin
      sStart := XML.GetAttribute(ATTR_ANGLE);
      sSweep := XML.GetAttribute(ATTR_SWEEP);
      if sStart <> '' then StartAngle := StrToIntDef(sStart, 0);
      if sSweep <> '' then SweepAngle := StrToIntDef(sSweep, 0);
    end
    else if XML.CurrentElement = TAG_SIZE then
    begin
      if XML.GetCoordsValue(iX, iY) then
      begin
        VirtualWidth := iX;
        VirtualHeight := iY;
      end
      else
        LogParsingError(THM_PRS_VIS_MALFORMED_GAUGE_SIZE_TAGS, Self);
    end;
  end;
end;

procedure TAHMDial.BuildProperties(Properties: TAHMUIPropertyList);
begin
{$IFDEF DESIGNER}
  inherited;

  // Build custom dial properties
  Properties.Add('Alignment', 'KnobAlign', CAT_KNOB, HINT_ALIGN);
  Properties.Add('Width', 'VirtualKnobWidth', CAT_KNOB, HINT_KNOBWIDTH);
  Properties.Add('Height', 'VirtualKnobHeight', CAT_KNOB, HINT_KNOBHEIGHT);
  Properties.Add('Center X', 'VirtualKnobCenterX', CAT_KNOB, HINT_KNOBX);
  Properties.Add('Center Y', 'VirtualKnobCenterY', CAT_KNOB, HINT_KNOBY);
  Properties.Add('Rotation X', 'VirtualRotateCenterX', CAT_KNOB, HINT_ROTATEX);
  Properties.Add('Rotation Y', 'VirtualRotateCenterY', CAT_KNOB, HINT_ROTATEY);
  Properties.Add('Images', 'KnobImages', CAT_KNOB, HINT_KNOBIMAGES);
  Properties.Add('Start Angle', 'StartAngle', CAT_DESIGN, HINT_STARTANGLE);
  Properties.Add('Sweep Angle', 'SweepAngle', CAT_DESIGN, HINT_SWEEPANGLE);
  Properties.Add('Backgrounds', 'BackgroundImages', CAT_DESIGN, HINT_BACKGROUNDS);
  Properties.Add('Width', 'VirtualWidth', CAT_DESIGN, HINT_WIDTH);
  Properties.Add('Height', 'VirtualHeight', CAT_DESIGN, HINT_HEIGHT);
{$ENDIF}
end;

end.
